linux 防火墙

Netfilter

Netfilter是Linux 2.4内核引入的全新的包过滤引擎,位于Linux内核中的包过滤功能体系,基于内核控制,实现防火墙的相关策略。Netfilter 由一些数据包过滤表组成,这些表包含内核用来控制信息包过滤的规则集。Netfilter在数据包必须经过且可以读取规则的位置,设有5个控制关卡。这5个关卡处的检查规则分别放在5个规则链中叫钩子函数(hook functions)。也就是说5条链对应着数据包传输路径中的5个控制关卡,链中的规则会在对应的关卡检查和处理。任何一个数据包,只要经过本机,必然经过5个链中的某个或某几个。

Netfilter中的五个规则链

规则链 作用
PREROUTING 数据包刚进入网络接口之后,路由之前
INPUT 数据包从内核流入用户空间
FORWARD 在内核空间中,从一个网络接口进入,到另一个网络接口去。转发过滤。
OUTPUT 数据包从用户空间流出到内核空间
POSTROUTING 路由后,数据包离开网络接口前

链其实就是包含众多规则的检查清单,每一条链中包含很多规则。当一个数据包到达一个链时,系统就会从链中第一条规则开始检查,看该数据包是否满足规则所定义的条件。如果满足,系统就会根据该条规则所定义的方法处理该数据包;否则就继续检查下一条规则,如果该数据包不符合链中任何一条规则,系统就会根据该链预先定义的默认策略来处理数据包。五条链中最常用的是 INPUT 和 OUTPUT 链。因为,我们的主机一般都是判断该是否接收数据,而转发流量一般较少。

数据包的传输过程

  1. 当一个数据包进入网卡时,它首先进入PREROUTING链,内核根据数据包目的IP判断是否需要转送出去。
  2. 如果数据包就是进入本机的,它就会沿着图向下移动,到达INPUT链。数据包到了INPUT链后,任何进程都会收到它。本机上运行的程序可以发送数据包,这些数据包会经过OUTPUT链,然后到达POSTROUTING链输出。
  3. 如果数据包是要转发出去的,且内核允许转发,数据包就会如图所示向右移动,经过FORWARD链,然后到达POSTROUTING链输出。

可以看出,刚从网络接口进入的数据包尚未进行路由决策,还不知道数据要走向哪里,所以进出口处没办法实现数据过滤,需要在内核空间设置转发关卡、进入用户空间关卡和离开用户空间关卡。

iptables

iptables是用来管理防火墙的的工具,属于静态防火墙,我们通过 iptables 将过滤规则写入内核,然后 Netfilter 再根据规则进行过滤数据包。所以实际上iptables是通过调用 Netfilter 来进行防火墙管理的,它本身不具备过滤数据包的功能。iptables程序位于 /sbin/iptables ,配置文件位于 /etc/sysconfig/iptables 。在Rhel7之前,防火墙是用 iptables

iptables中也有和netfilter中一模一样的5种规则链,还多了4个规则表。规则表的作用是容纳各种规则链。规则表的划分依据是防火墙规则的作用。四个表中常用的是 filter 表。最常用的链是 INPUT 和 OUTPUT 链

不指定表名时,默认指定 filter 表

iptables中的四个规则表

规则表 作用
raw表 确定是否对该数据包进行状态跟踪 kernel2.6之后加进去的
mangle表 为数据包设置标记
nat表 修改数据包中的源、目标ip或端口
filter表 确定是否放行该数据包(过滤)

img

规则表之间的匹配顺序: raw –>mangle–>nat–>filter

规则链之间的顺序:

入站:PREROUTING –> INPUT

出站:OUTPUT–>POSTROUTING

转发:PREROUTING–>FORWARD–>POSTROUTING

规则链的匹配顺序:

按顺序依次检查, 匹配到了即停止(LOG策略例外)

若找不到相匹配的规则,则按该链的默认策略处理

img

数据包的常见控制类型:

  • ACCEPT:允许通过
  • DROP:直接丢弃,不给出任何回应
  • REJECT:拒绝通过,必要时会给出提示
  • LOG:记录日志信息,然后传给下一条规则继续匹配

iptables的基本语法

语法构成:iptables 【-t 表名】 选项 链名 条件 【-j 控制类型】

iptables [-t 表名] 管理选项 [链名] [匹配条件] [-j 控制类型]

1.不指定表名时,默认指filter表
2.不指定链名时,默认指表内的所有链
3.除非设置链的默认策略,否则必须指定匹配条件
4.选项、链名、控制类型使用大写字母,其余均为小写

  • 不指定链名时,默认指定表内的所有链
  • 除非设置链的默认策略,否则必须指定匹配条件
  • 选项、链名、控制类型使用大写字母,其余均为小写

img

常用的控制类型

ACCEPT 允许数据包通过。
DROP 直接丢弃数据包,不给出任何回应信息。
REJECT 拒绝数据包通过,会给数据发送端一个响应信息。
SNAT 修改数据包的源地址。
MASQUERADE 伪装成一个非固定公网IP地址。
DNAT 修改数据包的目的地址。
PNAT 目标端口转换 端口修改
LOG 在/var/log/messages文件中记录日志信息,然后将数据包传递给下一条规则。LOG只是一种辅助动作,并没有真正处理数据包。

规则的匹配条件

通用匹配:可直接使用,不依赖于其他条件或扩展。包括网络协议、ip地址、网络接口等

  • 协议匹配:-p 协议名 iptables -A INPUT -p icmp -j DROP
  • 地址匹配:-s 源地址 、-d 目的地址 iptables -A INPUT -s 192.168.10.0/24 -j DROP
  • 接口匹配:-i 入站网卡 、-o 出站网卡 iptables -A INPUT -i eth0 -p icmp -j DROP

隐含匹配:要求以特定的协议匹配作为前提,包括端口、TCP标记、ICMP类型等条件

  • 端口匹配:–sport 源端口 、–dport 目的端口 iptables -A INPUT -p tcp --dprot 20:21 -j ACCEPT
  • TCP标记匹配:–tcp-flags 检查范围 被设置的标记
  • ICMP类型匹配:–icmp-type ICMP类型

显式匹配:要求以 “ -m 扩展模块” 的形式明确指出类型,包括多端口、MAC地址、IP范围、数据包状态等

  • 多端口匹配:-m multiport –sport 源端口列表 -m multiport –dport 目的端口列表 iptables -A INPUT -p tcp -m multiport --dport 25,80,110,143 -j ACCEPT
  • IP范围匹配:-m iprange –src-range IP范围 iptables -A FORWARD -p tcp -m iprange --src-range 192.168.4.21-192.168.4.28 -j ACCEPT
  • MAC地址匹配: -m mac –mac-source MAC地址 iptables -A INPUT -m mac --mac-source 00:0c:29:c0:55:3f -j DROP

img

添加新的规则:

-A:在链的末尾追加一条规则
-I :在链的开头(或指定序号插入)一条规则

  • iptables -t filter -A INPUT -p tcp -j ACCEPT
  • iptables -A INPUT -p tcp -j ACCEPT
  • iptables -I INPUT -p udp -j ACCEPT
  • iptables -I INPUT 3 -p icmp -j ACCEPT 在filter表中的INPUT链中的第3条加入一条允许所有icmp协议的规则

删除,清空规则(暂时,重启失效)

-D:删除链内指定序号的一条规则
-F:清空所有的规则

  • iptables -D INPUT 3
  • iptables -t raw -F
  • iptables -F

查看规则列表

-L:列出所有的规则条目 (默认查看的是filter表)
-n:以数字形式显示地址、端口等信息 (地址用数字表示)
-v:以更详细的方式显示规则信息(显示比特流,发包的数量等信息)
–line-numbers:查看规则时,显示规则的序号

  • iptables -L
  • iptables -nvL

设置默认规则

-P:为指定的链设置默认规则

  • iptables -t nat -P OUTPUT ACCEPT

img

规则的备份和还原

导出( 备份规则):iptables-save > /tmp/xie/1.txt

//将iptables配置文件保存到 /tmp/xie/1.txt文件中

还原规则:iptables-restore < /tmp/xie/1.txt

规则永久生效:

iptables不是一个守护进程,我们修改只是当前的修改,重启服务或重启系统就会失效。要想使规则永久生效,需要保存规则: service iptables save ,使我们当前的配置保存到配置文件 /etc/sysconfig/iptables 中,然后重启服务或者重启主机,使规则加载到内存,才能使配置永久生效。防火墙的运行需要建立在网络配置正常的前提下。

services iptables start / stop / restart 开启/停止/重启 iptables服务

chkconfig --level 5 iptables on / off

iptables做本地端口转发

开启数据转发功能:/etc/sysctl.conf 设置 net.ipv4.ip_forward=1

1
2
3
4
5
6
7
iptables -t nat -L         #查看端口转发记录
##将192.168.10.21的2222端口的流量都转发给192.168.10.21的22端口
iptables -t nat -A PREROUTING \
-d 192.168.10.21 -p tcp --dport 2222 \
-j DNAT --to-destination 192.168.10.21:22
iptables -save #保存
iptables -t nat -F #清除规则

做本地端口转发,将本地2222端口的流量转发给22端口

img

示例

SNAT

源地址转换 (Source Network Address Translation)

实现内网主机通过防火墙进行上网,需要使用SNAT(源地址转换POSTROUTING)

1
2
3
4
5
6

iptables -t net -A POSTROUTING -s 192.168.100.0/24 -o eth0 -j SNET --to-source 192.168.200.10

# 指定从哪个ip地址转换出去(静态公网地址)
iptables -t nat -I POSTROUTING -s 172.16.1.0/24 -j SNAT --to 10.0.0.200
iptables -t nat -L -n

MASQUERADE

伪装

1
2
# 当外网源地址为动态获取的地址时,MASQUERADE可自行判断要转换为的外网地址
iptables -t nat -A POSTROUTING -s 172.16.1.0/24 -j MASQUERADE

DNAT

目标地址转换 (Destination Network Address Translation)

实现外网主机通过防火墙访问内部主机80端口,需要通过DNAT(目标地址转换PREROUTING)

1
2
# 地址映射【DNAT】
iptables -t nat -I PREROUTING -d 10.0.0.200 -j DNAT --to 172.16.1.7

PNAT

目标端口转换

1
2
3
4
5
6
7
8
9
10
# 端口映射【DNAT】
iptables -t nat -I PREROUTING \
-d 10.0.0.200 -p tcp --dport 80 \
-j DNAT --to 172.16.1.7:80
iptables -t nat -L -n

#把来自80端口的访问映射到本机的8080端口
iptables -A PREROUTING -t nat \
-d 192.168.10.11 -p tcp --dport 80 -j REDIRECT --to-ports 8080

Firewalld

Firewalld自身并不具备防火墙的功能,而是和iptables一样需要通过内核的Netfilter来实现,也就是说firewalld和iptables一样,他们的作用都是用于维护规则,而真正使用规则干活的是内核的Netfilter,只不过Firewalld和iptables的结构以及使用方法不一样罢了。iptables是动态防火墙 dynamic firewall daemon 。支持ipv4和ipv6。Rhel7中默认将防火墙从iptables升级为 firewalld。firewalld相对于iptables主要的优点有:

  • firewalld在使用上要比iptables人性化很多,即使不明白“四张表五条链”而且对TCP/IP协议也不理解也可以实现大部分功能。

firewalld的配置模式

firewalld的配置文件以xml格式为主(主配置文件firewalld.conf例外),他们有两个存储位置

  1. /etc/firewalld/services/ 用户自定义配置文件
  2. /usr/lib/firewalld/services/ 系统配置文件,预置文件,尽量不要修改

img

过滤规则集合:zone

  • 一个zone就是一套过滤规则,数据包必须要经过某个zone才能入站或出站。不同zone中规则粒度粗细、安全强度都不尽相同。可以把zone看作是一个出站或入站必须经过的安检门,有的严格、有的宽松、有的检查的细致、有的检查的粗略。

  • 每个zone单独对应一个xml配置文件,在目录 /usr/lib/firewalld/services/

  • 每个zone都有一个默认的处理行为,包括:default(省缺)、 ACCEPT、REJECT、DROP

  • firewalld提供了9个zone:

    • drop 任何流入的包都被丢弃,不做任何响应。只允许流出的数据包。
    • block   任何流入的包都被拒绝,返回icmp-host-prohibited报文(ipv4)或icmp6-adm-prohibited报文(ipv6)。只允许由该系统初始化的网络连接
    • public 默认的zone。部分公开,不信任网络中其他计算机,只放行特定服务。
    • external 只允许选中的服务通过,用在路由器等启用伪装的外部网络。认为网路中其他计算器不可信。
    • dmz 允许隔离区(dmz)中的电脑有限的被外界网络访问,只允许选中的服务通过。
    • work 用在工作网络。你信任网络中的大多数计算机不会影响你的计算机,只允许选中的服务通过。
    • home 用在家庭网络。信任网络中的大多数计算机,只允许选中的服务通过。
    • internal 用在内部网络。信任网络中的大多数计算机,只允许选中的服务通过。
    • trusted 允许所有网络连接,即使没有开放任何服务,那么使用此zone的流量照样通过(一路绿灯)。

防火墙默认只激活public区域,不指定区域,默认是 public 区域

防火墙状态监测

  • firewall-cmd --list-all //列出所有的已配置接口的配置信息
  • firewall-cmd --state //查看防火墙的状态
  • firewall-cmd --get-active-zones //查看防火墙激活的zone区域
  • firewall-cmd --get-services //查看防火墙预定义的服务
  • firewall-cmd --get-default-zone //查看防火墙默认的zone区域
  • firewall-cmd --set-default-zone=home //将防火墙默认的区域设置为home区域
  • firewall-cmd --get-service --permanent //查看永久生效的服务
  • firewall-cmd --list-rich-rules //列出所有的富规则

防火墙的配置

在区域中添加服务或端口:

临时设置:firewall-cmd -zone=public --add-service=mysql

firewall-cmd --add-port=8080-8081/tcp //不写区域的话默认是public区域

在区域中移除服务或端口:

临时设置:firewall-cmd --remove-service=https

firewall-cmd --remove-port=8080-8081/tcp

在区域中永久添加和移除服务或端口(–permanent参数):

永久设置:firewall-cmd --permanent --add-service=mysql

firewall-cmd --permanent --remove-port=8080-8081/tcp

firewall-cmd --reload //重载,永久设置需要重载配置文件才能生效

富规则:富规则允许你创建更复杂的配置

  • firewall-cmd --zone=public --add-rich-rule="rule family="ipv4" source address="192.168.10.0/24" service name=ssh accept " //zone区域添加一个富规则,允许来自源网段192.168.10.0/24 的数据
  • firewall-cmd --permanent --zone=public --add-rich-rule="rule family="ipv4" source address="192.168.10.0/24" port protocol="tcp" port="8080" accept" //在zone区域中永久添加一个富规则,允许来自源网段 192.168.10.0/24 的tcp协议的8080端口的数据
  • firewall-cmd --zone=public --remove-rich-rule="rule family="ipv4" source address="192.168.10.0/24" service name="http" accept " //移除zone区域的富规则–来自192.168.10.0/24网段的http服务

防火墙的选择

Rhel7中既有iptables防火墙,又有firewalld防火墙。但是同一时刻只能开一个,所以必须得关闭其中一个防火墙。一般我们都是关闭iptables防火墙,而打开firewalld防火墙,因为firewalld比iptables强大太多了。

开启firewalld防火墙,并且使其开机自启动,关闭iptables防火墙

  • systemctl mask iptables.service //屏蔽iptables服务
  • systemctl start firewalld.service
  • systemctl enable firewalld.service

打开路由转发功能 :

  • echo 1 > /proc/sys/net/ipv4/ip_forward
  • sysctl -w net.ipv4.ip_forward=1
  • vim /etc/sysctl.conf , 将net.ipv4.ip_forward=0 改为 =1,然后 sysctl -p /etc/sysctl.conf 使之生效 (永久开启IP转发)

端口转发:

1
2
3
4
5
6
7
# 将外部访问本机10000端口的流量转发至本机22号端口
firewall-cmd --zone=public \
--add-rich-rule="rule family=ipv4 forward-port port=10000 protocol=tcp to-port=22"

# 将外部访问本机8080端口流量转到192.168.10.24上的80端口
firewall-cmd --zone=public \
--add-forward-port=port=8080:proto=tcp:toport=80:toaddr=192.168.10.24

示例

工作流图

下面这张图描述了一个L3的IP封包如何通过iptables:

packet_flow10

对于此图的说明:

  1. Iptables和内核路由的关系:执行完PREROUTING链之后,会进行路由表的查询
  2. 通过lo接口的封包,不走PREROUTING的DNAT表
  3. 出站封包在OUTPUT链之前就进行了路由处理。但是如果OUTPUT进行了DNAT,则会进行重新选路
  4. 入站封包,如果使用了隧道,则会经由PREROUTING - INPUT链逐层的解除隧道
  5. 出站封包,如果使用了隧道,则会经由OUTPUT -POSTROUTING链逐层的进行隧道封装

下面的图示意了更接近实际的流程:

tables_traverse

基础

iptables是用户空间命令,通过netlink和内核的netfilter模块进行交互,在L3/L4操控封包。

规则链

netfilter是Linux网络安全大厦的基石,从2.4开始引入。它提供了一整套Hook函数机制,IP层的5个钩子点对应了iptables的5个内置链条:

  1. PREROUTING,在此DNAT
  2. POSTROUTING,在此SNAT
  3. INPUT,处理输入给本地进程的封包
  4. OUTPUT,处理本地进程输出的封包
  5. FORWARD,处理转发给其他机器、其他网络命名空间的封包

对于从网络接口入站的IP封包,首先进入PREROUTING链,然后进行路由判断:

  1. 如果封包路由目的地是本机,则进入INPUT链,然后发给本地进程
  2. 如果封包路由目的地不是本机,并且启用了IP转发,则进入FORWARD链,然后通过POSTROUTING链,最后经过网络接口发走

对于本地进程发往协议栈的封包,则首先通过OUTPUT链,然后通过POSTROUTING链,最后经过网络接口发走

除了链条,iptables还包含一个正交的概念,表是用于分类管理iptables规则的。不同的表,通常用作不同的目的:

  1. filter表:用于控制到达某条链条上的数据包如何处理,是放行(Accept)、丢弃(Drop)还是拒绝(Reject)。如果不指定-t参数,这是默认操控的表格
  2. nat表:用于修改源地址,或者目的地址。该表格在某个创建了新连接的封包出现时生效
  3. mangle表:该表格用于修改封包的内容
  4. raw表:具有高优先级,可以对接收到的封包在连接跟踪(Connection Tracking,为了支持NAT,iptables会对每个L4连接进行跟踪,也就是维护每个L4连接的状态)前进行处理(也就是取消跟踪),可以应用在那些不需要做NAT的情况下提高性能。例如高访问的Web服务,可以不让iptables做80端口封包的连接跟踪
  5. security表:用于强制访问控制(Mandatory Access Control),很少使用

表的优先级

表的优先级从高到低为 raw ⇨ mangle ⇨ nat ⇨ filter ⇨ security。用户不能自定义新表。

封包会依次经过相关的链,在每个链中,会根据表的优先级,依次遍历各个表中的规则。任何表中的规则都有机会拒绝封包。

表和链的关系

需要注意,不是任何链条上可以挂任何表

  1. raw可以挂在PREROUTING、OUTPUT
  2. mangle可以挂在任何链上
  3. nat(SNAT)可以挂在POSTROUTING、INPUT
  4. nat(DNAT)可以挂在PREROUTING、OUTPUT
  5. filter可以挂在FORWARD、INPUT、OUTPUT
  6. security可以挂在FORWARD、INPUT、OUTPUT

mangle表

用于修改封包。某些目标只能用在mangle表中,包括:

  1. TOS,此目标设置封包的Type Of Servier字段,以便影响封包的处理策略,例如如何进行路由
  2. TTL,设置封包的Time To Live字段
  3. MARK,给封包添加一个标记值(数字),iproute2能够识别此mark并且进行特殊的路由处理。此外基于MARK还可以进行带宽限制、基于Class的排队

nat表

应当仅仅用于网络地址转换。也就是使用以下目标:

  1. DNAT:用在你有一个公共地址,别人访问此地址时,你需要将访问重定向到防火墙背后的某个服务时
  2. SNAT:允许隐藏在防火墙背后的内网机器访问外部网络
  3. MASQUERADE:类似SNAT,不同之处在于每次都需要动态计算使用什么作为转换后的源地址。用在主机地址不固定的情况下
  4. REDIRECT:类似于DNAT,但是新的目的地址被锁定为接收封包的那个网卡地址,同时端口改为随机的或指定的值

上面这些目标都会改变IP封包的首部。

filter表

用于过滤封包,使用ACCEPT、DROP之类的目标

规则

表中存放的是一个个规则,规则由匹配+目标组成。目标包括:

  1. ACCEPT,允许封包通过,如果在子链ACCEPT,则相当于在父链也ACCEPT了,不会继续遍历父链后续规则
  2. RETURN,从当前链返回,行为类似于编程语言函数中的return。如果当前:
    1. 位于子规则链,则返回到父链调用子链的那个规则的下一条规则处执行
    2. 不位于子规则链,则执行默认策略
  3. DROP,直接丢弃数据包,不进行后续处理
  4. REJECT,返回connection refused或者destination unreachable报文
  5. QUEUE,将数据包放入用户空间的队列,供用户空间应用程序处理
  6. JUMP,跳转到用户自定义链继续执行
  7. LOG 让内核记录匹配的封包
  8. AUDIT 对封包进行审计
  9. DNAT 目标地址映射
  10. SNAT 源地址映射
  11. MASQUERADE 源地址映射,用于本机地址是动态获取的情况下

通常情况下,一旦匹配了某个规则,就会执行它的目标。同时,不再检查同规则链、同表的后续规则,这意味着规则的顺序非常重要。某些目标是例外,例如LOG、MARK,会继续遍历后面的规则

如果内置规则链执行到结尾****,或者内置规则链中的某个目标是RETURN的规则匹配封包,那么规则链策略(Chain Policy)中定义的目标将决定如何处理当前封包。

封包遍历步骤

目的地是本机的

步骤 说明
1 封包在网络上
2 封包进入本机网卡
3 mangle PREROUTING 在此修改封包,例如改变TOS。连接跟踪发生在这个链上
4 nat PREROUTING 主要用于DNAT。避免在此进行封包过滤,某些情况下可能被bypass
5 路由决策:封包目的地是本机,还是需要被转发,转发到哪里
6 mangle INPUT 路由决策之后,发送到本机进程之前修改封包
7 filter INPUT 针对所有目的地是本地的封包进行过滤,不管它来自什么网络接口
8 封包到达本地进程

从本机发起的

步骤 说明
1 本地进程发送封包
2 路由决策:使用什么源地址,什么出口网卡,收集一些其它信息
3 mangle OUTPUT 修改封包。不要在此进行封包过滤,因为具有副作用。本地生成的连接跟踪也在此进行
4 nat OUTPUT 对出站封包进行NAT
5 再次路由决策,因为mangle或nat可能导致需要重新路由
6 filter OUTPUT 对本机发出的封包进行过滤
7 mangle POSTROUTING 在路由决策之后,发送出网卡之前,修改封包
8 nat POSTROUTING 在此进行SNAT。不要在此进行封包过滤,即使默认策略设置为DROP也可能导致某些封包溜走
9 到达网卡
10 进入网线

需要转发的

步骤 说明
1 封包在网线上
2 封包到达入口网卡
3 mangle PREROUTING 修改封包
4 nat PREROUTING 进行DNAT
5 路由决策
6 mangle FORWARD 仅用于特殊场景下,在初始路由决策之后修改封包
7 filter FORWARD 对转发封包进行过滤
8 最终路由决策
9 mangle POSTROUTING 所有路由决策完成之后,修改封包
10 nat POSTROUTING 进行SNAT
11 到达出口网卡
12 进入网线

连接跟踪状态机

连接跟踪(Connection tracking )的目的是,让Netfilter框架能够知晓每个连接的状态。你可以使用 –state来匹配状态。

注意,为了更加准确的跟踪TCP连接,内核还维护一系列的内部状态。这些内部状态不能用来匹配。

在iptables中,每个封包都可能和一个被跟踪的连接相关联。连接可以处于4种不同的状态:

状态 说明
NEW 说明看到的封包是连接的第一个封包。导致此状态的例子:TCP协议的SYN包
ESTABLISHED 当双向封包都监测到之后,进入此状态。导致此状态的例子:TCP协议的应答SYN/ACK
RELATED 如果连接和另外一个ESTABLISHED状态的连接相关,则它进入此状态如果主连接产生一个新连接,这个新连接就是RELATED的。例如FTP数据连接是相关于FTP控制连接的
INVALID 无法识别的状态,可能由于系统内存耗尽、不响应已知连接的ICMP报文导致

在内核中,连接跟踪由conntrack模块负责。

除了本地生成的包,在OUTPUT中跟踪之外,所有连接跟踪都在PREROUTING链中进行。例如:

  1. 本地发起一个连接的初始封包,则在OUTPUT链中,连接变为NEW
  2. 当接收到上述封包的应答包后,在PREROUTING链中,连接变为ESTABLISHED

如果外部发起初始封包,则NEW状态是在PREROUTING中产生的。

命令

格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
iptables [-t table] {-A|-C|-D} chain rule-specification
ip6tables [-t table] {-A|-C|-D} chain rule-specification
iptables [-t table] -I chain [rulenum] rule-specification
iptables [-t table] -R chain rulenum rule-specification
iptables [-t table] -D chain rulenum
iptables [-t table] -S [chain [rulenum]]
iptables [-t table] {-F|-L|-Z} [chain [rulenum]] [options...]
iptables [-t table] -N chain
iptables [-t table] -X [chain]
iptables [-t table] -P chain target
iptables [-t table] -E old-chain-name new-chain-name

其中:
rule-specification = [matches...] [target]
match = -m matchname [per-match-options]
target = -j targetname [per-target-options]

输出

带-n参数时,0.0.0.0/0表示匹配任何地址(anywhere)
不带-n参数时,所有保留私有地址都会显示为bogon

选项

选项被分为若干组:

指定表

-t –table table 指定命令操控的表格的名称,如果内核被配置为自动模块加载方式,并且相应模块不存在,将尝试自动加载适当的模块

子命令

该组选项指定期望执行的动作,仅其中一个可以在命令行中指定
-A –append chain rule-specification 附加一个或者多个规则到目标规则链。如果源/目标名称解析到多于一个地址,那么规则将会添加到每个可能的地址组合
-C –check chain rule-specification 检查目标规则链是否具有匹配的规则,该命令与-D的逻辑类似,但是不会修改当前的iptables配置,使用其退出码来指示是否成功
-D –delete chain rule-specification 从目标规则链中删除一个或者多个规则,需要指定规则的匹配规则
-D –delete chain rulenum 从目标规则链中删除一个或者多个规则,需要指定从1开始的个数
-I –insert chain [rulenum] rule-specification 插入一个或者多个规则到目标规则链的指定位置,如果rulenum是1则插到最前面,举例: -I FORWARD 1
-R –replace chain rulenum rule-specification 替换目标规则链中的一个或者多个规则。如果源/目标名称解析到多个地址,该命令将失败
-L –list [chain] 列出选中规则链中的所有规则,如果不指定规则链,则列出全部规则链,与其他命令不同,该命令仅应用到特定的表格,因此如果想列出NAT表的全部规则,需要使用:iptables -t nat -n -L。该命令一般与-n联用,以避免耗时的DNS查找,指定-Z选项也是合法的
-S –list-rules [chain] 打印选定规则链中的所有规则,该命令仅应用到特定的表格
-F –flush [chain] 刷空选定规则链中的所有规则(如果不指定规则链,则应用到表格中全部的规则链),等价于一条条删除规则
-Z –zero [chain [rulenum]] 将封包、字节计数器置零
-N –new-chain chain 创建一个用户自定义的规则链,名称为chain,chain必须不是任何已经存在的target
-X –delete-chain [chain] 删除可选的、用户定义的规则链
-P –policy chain target 设置某个规则链的策略(没有规则匹配封包时的默认行为)为target。仅内置规则链可以设置策略,策略的目标不得是任何规则链,举例:

1
iptables --policy INPUT DROP  # 设置INPUT的规则链策略为DROP

-E –rename-chain old-chain new-chain 重命名用户定义的规则链

参数

以下的参数用来组成一个rule specification
[!] -p, –protocol protocol 用来检查的规则或者封包的协议。支持的值包括:tcp, udp, udplite, icmp, icmpv6,esp, ah, sctp, all。叹号用来反转测试,默认all
[!] -s, –source address[/mask][,…] 封包来源规则,address可以是网络名、主机名、IP地址(附加/掩码)、或者普通IP地址。主机名仅仅会在规则提交到内核前解析一次。叹号用来反转地址规则。指定多个地址是可以的,但是会被分解为多个规则,或者导致多个规则被删除(-D)
[!] -d, –destination address[/mask][,…] 封包目标规则,类似来源规则
-m –match match 指定使用的match,所谓match是用来测试特定属性的扩展模块
-j –jump target 指定规则的目标,即:当规则匹配的时候要做什么。目标可以是用户定义的规则链、一个特殊的内置目标(立即决定封包命运)、或者一个扩展。如果某个规则不设置该选项,那么匹配后将对封包不构成任何影响,但是规则的计数器会增加
-g –goto chain 仅仅用在用户定义的规则链中,指示重定向到另外一个链继续处理。和-j不同,-j后子链完成匹配后会跳转到父链,-g则不会跳转,如果子链没有匹配的规则则按子链的默认策略处理
[!] -i, –in-interface name 仅用于进入INPUT, FORWARD, PREROUTING链的封包。限制接收到封包的网络接口名称,如果名称后面跟着+号,所有以该名称开头的接口都被匹配
[!] -o, –out-interface name 仅用于进入FORWARD, OUTPUT, POSTROUTING链的封包。限制封包将被送出的网络接口名称,如果名称后面跟着+号,所有以该名称开头的接口都被匹配
[!] -f, –fragment 对于分片的IP数据报,该规则将仅仅用于第二个或者以后的的分片。仅用于IPv4  
-c –set-counters packets bytes 在INSERT, APPEND, REPLACE规则时,用于初始化封包、字节计数器

其他选项

-v –verbose 冗长输出,list命令将输出接口名称、规则选项、TOS掩码、包以及字节计数器。对于附加、插入、删除、替换规则的操作,该选项导致规则的详细信息被打印
-w –wait 等待xtables锁被解开,为了防止多个进程同时修改,iptables具有锁定机制
-n –numeric 数字化输出,IP地址和端口将使用数字格式
-x –exact 仅与-L命令有关,显示精确的封包、字节计数,使用1024而不是1000作为倍率
–line-numbers 列出规则时,显示行号
–modprobe=command 当添加、删除规则时,使用命令command加载必要的模块(目标、匹配-m扩展等)

应用举例

透明代理

1
2
3
4
5
6
7
8
9
10
11
12
# 发送任何目的端口为25的封包给在127.0.0.1:10025上监听的进程
# 并且给封包以标记 1
iptables -t mangle -A PREROUTING -p tcp --dport 25 -j TPROXY \
--tproxy-mark 0x1/0x1 --on-port 10025 --on-ip 127.0.0.1


# PREROUTING链之后,转发之前,检查路由表
# 如果发现封包被标记为 1,那么查找100号表
ip rule add fwmark 1 lookup 100

# 100号路由表,将封包通过lo发出。注意出口为lo的封包不会真正路由,而是交给本地进程处理
ip route add local 0.0.0.0/0 dev lo table 100

网关

1
2
3
4
5
6
7
8
9
10
# 对于准备从wlan0出去的封包,进行源地址映射
iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE

# 允许转发来自eth0、发往wlan0的封包
iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT

# 对于来自wlan0、发往wlan0的封包,如果状态为RELATED,ESTABLISHED则允许通过
# ESTABLISHED,封包属于一个已经建立的连接
# RELATED,封包创建了新连接,但是此连接和一个已经存在的连接相关,例如FTP数据传输器、ICMP错误
iptables -A FORWARD -i wlan0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT

防火墙

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 列出filter表(可用于实现防火墙)内容。注意默认网卡信息不显示,需要加 -v
iptables -t filter -L -n

# 禁止向目标地址发送网络包
iptables -A OUTPUT -j DROP -d 10.5.39.223
# 撤销
iptables -D OUTPUT -j DROP -d 10.5.39.223

# 禁止PING
iptables -I INPUT -p icmp --icmp-type 8 -j DROP
# 撤销
iptables -D INPUT -p icmp --icmp-type 8 -j DROP

# 禁止向目标网络转发包
iptables -A FORWARD -d 172.27.0.0/16 -j DROP
iptables -D FORWARD -d 172.27.0.0/16 -j DROP

# 取消Docker的网络隔离
iptables -I DOCKER-USER 1 -j ACCEPT
# 或者 两个Docker网络的网段
iptables -t filter -I DOCKER-USER -s 172.17.0.0/16 -j ACCEPT
iptables -t filter -I DOCKER-USER -s 172.21.0.0/16 -j ACCEPT

优化输出

1
2
# 显示Filter表的详细信息,格式化输出
sudo iptables -L -nv -t filter | column -t -s " "

源地址映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 作为VPN服务器的主机,启用ipv4转发
iptables -t nat -L -v -n
# 输出如下:
# ...
# Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
# pkts bytes target prot opt in out source destination
# 66404 4468K MASQUERADE all -- any any anywhere anywhere

# 删除既有的IP掩蔽设置,针对所有封包做了端口映射
iptables -t nat -D POSTROUTING -j MASQUERADE
# 这时再连接到gmem.cc的VPN,发现无法上外网了
# 原因是,没有合适的端口映射,gmem.cc无法将封包发回给VPN客户端

# 修改后的IP掩蔽配置,限制了源IP地址的范围、出口网卡
iptables -t nat -A POSTROUTING -m iprange --src-range 172.21.0.100-172.21.0.120 -o eth0 -j MASQUERADE

目的地址映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 把针对本机192.168.0.89的7777端口的请求全部转换为对虚拟机172.16.87.132的80端口的请求
sudo iptables -t nat -A PREROUTING # 入站后第一个链
-p tcp --dport 7777 -i eth0 # 如果封包来自eth0且目标端口为7777
-j DNAT --to-destination 172.16.87.132:80 # 那么修改目标地址为虚拟机的80端口

# 此时,针对192.168.0.89:7777的封包,目的地址已经改为172.16.87.132
# 允许对目的地址为172.16.87.132的封包进行转发,可能发生的情况是:
# 原始数据报
# 192.168.0.200:23387 => 192.168.0.89:7777
# 入站后,目标地址被映射:
# 192.168.0.200:23387 => 172.16.87.132:80
# 转发:由于目的地址不是本机,因此进行转发,ipv4转发功能已经开启过
# 出站前,源地址被映射被映射:
# 172.16.87.1:39480 => 172.16.87.132:80
# 这就为本机39480端口和192.168.0.200:23387建立了映射关系
sudo iptables -t nat -A POSTROUTING -p tcp -d 172.16.87.132 --dport 80 -j MASQUERADE
# 上面的命令也可以替换为:
sudo iptables -t nat -A POSTROUTING -p tcp -d 172.16.87.132 --dport 80 -j SNAT --to-source 172.16.87.1
# 返回的数据报,处理时,自动逆向映射:原先的目标映射用于逆向源映射,原先的源映射用于逆向目标映射
# 原始数据报
# 172.16.87.132:80 => 172.16.87.1:39480
# 入站后,目标地址被映射
# 172.16.87.132:80 => 192.168.0.200:23387
# 出站前,源地址被映射
# 192.168.0.89:7777 => 192.168.0.200:23387

# ✭✭✭ 一个重要的原则是,仅仅去匹配初始发起连接的包,做好NAT后,返回的包自动映射

编写规则

很多iptables命令都要求提供一个规则的规格(rule-specification,后面简称规则)。规则实际上就是一系列iptables命令选项,大部分选项都用来说明如何匹配封包的, -j用于指定目标。

添加注释

要为规则添加注释,参考:

1
2
iptables -m comment --comment "comment here"
iptables -A INPUT -i eth1 -m comment --comment "my LAN - " -j DROP

可以看到,注释以匹配扩展的形式被支持。

注释最多256字符,执行 iptables -L命令时会以 /* */打印在规则尾部。

一般匹配

这些匹配选项的值,前面都可以加 !表示反向匹配。

选项 说明
-p, –protocol 匹配协议,可选值 TCP, UDP, ICMP等
-s, –src, –source 匹配源地址。地址形式192.168.0.0/24
-d, –dst, –destination 匹配目的地址。地址形式192.168.0.0/24
-i, –in-interface 匹配入站网络接口,仅用于INPUT, FORWARD, PREROUTING链。eth+表示匹配任何以太网卡
-o, –out-interface 匹配出站网络接口,OUTPUT, FORWARD, POSTROUTING链。eth+表示匹配任何以太网卡
-f, –fragment 用于匹配分片的封包的非首分片。取反需要 ! -f

隐式匹配

用于TCP的匹配

选项 说明
–sport, –source-port 源端口。支持端口范围: --source-port 22:80
–dport, –destination-port 目的端口。支持端口范围
–tcp-flags 匹配TCP标记,可用的标记SYN, ACK, FIN, RST, URG, PSH,还有两个特殊的值ALL, NONE分别匹配具有任何标记的封包、没有任何标记的封包
–syn 匹配设置了SYN位,但是没有设置ACK,RST位的封包
–tcp-option 基于TCP选项来匹配

举例:

1
2
# 匹配SYN标记被设置,ACK,FIN,RST没有被设置的封包
iptables -A FORWARD -p tcp --tcp-flags SYN,ACK,FIN,RST SYN

用于UDP的匹配

选项 说明
–sport, –source-port 源端口匹配
–dport, –destination-port 目的端口匹配

用于ICMP的匹配

选项 说明
–icmp-type 匹配ICMP报文类型

显式匹配

所谓显式匹配,是指必须明确通过 -m或 –match选项加载的匹配。

conntrack

内核中基于netfilter框架实现的连接跟踪模块即conntrack。在DNAT时,conntrack使用状态机来跟踪连接的状态,记住目的地址被从什么改成了什么,这样才能将回程包的源地址改回来

基于连接跟踪的匹配。使用 -m conntrack加载,可以接额外选项:

子选项 说明
–ctstate 匹配封包状态,可选值:INVALID 匹配无法识别或没有任何状态的数据报 ESTABLISED 匹配连接请求的响应,以及后续的包。此时conntrack知道封包属于哪个连接 NEW 匹配连接的第一个包,conntrack此时对连接一无所知,通常发生在SYN时 RELATED 当一个连接和另外一个ESTABLISHED状态的连接相关时,则前一个连接为此状态 SNAT 已被SNAT DNAT 已被DNAT要匹配多个状态,用逗号分隔: -m conntrack --ctstate ESTABLISHED,RELATED取反需要在匹配模块名之后: -m conntrack ! --ctstate ESTABLISHED,RELATED
–ctproto 匹配封包协议。示例 -m conntrack ! --ctproto TCP
–ctorigsrc 匹配封包源地址。示例 -m conntrack --ctorigsrc 192.168.0.0/24
–ctorigdst 匹配封包目的地址

iprange

使用IP地址范围进行匹配。使用 -m iprange加载,可以接额外选项:

子选项 说明
–src-range 源地址范围。示例 -m iprange --src-range 192.168.1.13-192.168.2.19
–dst-range 目的地址范围。示例 -m iprange --dst-range 192.168.1.13-192.168.2.19

length

第三层载荷匹配。该扩展用于根据网络层载荷,即传输层封包的长度进行匹配

用法: -m length [!] –length length[:length] TCP/UDP包的长度

limit

可以用于限制特定规则的日志记录的频度。使用 -m limit加载,可以接额外选项:

子选项 说明
–limit 指定平均匹配速率。如果指定单位时间内匹配的封包超过数量,则不再匹配示例: -m limit –limit 3/hour。每小时最多匹配3次可用单位:/second /minute /hour /day
–limit-burst 指定最大匹配速率

mac

根据源地址的MAC地址进行匹配,只能用在PREROUTING, FORWARD,INPUT 规则链。

示例: -m mac --mac-source 00:00:00:00:00:01

mark

根据关联当前封包的netfilter标记进行匹配。mark是一个特殊字段,无符号整数(unsigned int),仅仅在内核中维护(不会嵌入在封包里)。封包在通过主机时,标记可以和它关联。

很多内核例程都会使用mark,从而完成流量塑形(Traffic Shaping)、过滤等工作。

要设置一个mark,可以使用iptables的MARK目标。由于以前在ipchains的FWMARK目标中设置,现在仍然会使用FWMARK这个术语。

示例: -m mark --mark 1986,匹配具有1986标记的封包。你还可以指定标记的掩码,也就是 -m mark –mark 1/1这种形式,这样 / 后面的数字,会与netfilter标记先进行逻辑与,得到的值再和 / 前面的值进行比较 —— 这可以仅仅关注fwmark的某个位

multiport

多端口匹配。该扩展只能用于以下协议:tcp, udp, udplite, dccp, sctp。最多指定15个端口

指定端口的语法: port[,port|,port:port]

使用 -m multiport加载,可以接额外选项:

子选项 说明
–source-port 根据源端口匹配,示例 -m multiport –source-port 22,53,80,110
–destination-port 根据目的端口匹配
–port 根据源或目的端口匹配

owner

根据创建封包的进程的身份来匹配封包,很明显,只能用于本机产生的封包上,你只能在OUTPUT链中使用该匹配。

使用 -m owner加载,可以接额外选项:

子选项 说明
–uid-owner 如果封包是由指定的User ID创建,则匹配可能的应用场景:仅仅允许Apache通过80端口发出数据
–gid-owner 如果封包是由指定的Group ID中的用户创建,则匹配
–pid-owner 如果创建封包的进程具有指定的PID,则匹配
–sid-owner 如果创建封包的进程具有指定的Session,则匹配

pkttype

根据链路层类型匹配。用法 -m pkttype [!] --pkt-type {unicast|broadcast|multicast}

state

和内核中的连接跟踪代码配合工作。此匹配会从连接跟踪状态机中读取封包的状态。

示例: -m state --state RELATED,ESTABLISHED

tcpmss

根据TCP的Maximum Segment Size来匹配,仅仅针对SYN或SYN/ACK包。

示例: iptables -A INPUT -p tcp --tcp-flags SYN,ACK,RST SYN -m tcpmss --mss 2000:2500

tos

根据Type Of Service来匹配。示例: iptables -A INPUT -p tcp -m tos --tos 0x16

ttl

根据Time To Live来匹配。示例: iptables -A OUTPUT -m ttl --ttl 60

addrtype

通过封包的地址类型进行匹配,可用的地址类型包括:

地址类型 说明
UNSPEC 未指定的地址,例如0.0.0.0
UNICAST 单播地址
LOCAL 本地地址(任何分配到任一本地网络接口的地址,包括loopback地址、ipvs虚拟地址)
BROADCAST 广播地址
ANYCAST 选播地址
MULTICAST 黑洞地址
UNREACHABLE 不可达地址
PROHIBIT 禁止地址

使用 -m addrtype加载,可以接额外选项:

子选项 说明
[!] –src-type type 根据源地址类型匹配地址类型:LOCAL:不是指loopback地址,而是指任何分配给本机某个网络接口的IP地址(包括loopback地址、ipvs虚拟地址)
[!] –dst-type type 根据目标地址类型匹配
–limit-iface-in 限制检查的入站网络接口,仅支持PREROUTING, INPUT, FORWARD规则链
–limit-iface-out 限制检查的出站网络接口,仅支持POSTROUTING, OUTPUT, FORWARD规则链

connbytes

根据某个连接到目前为止发送的字节数、封包数,或者每封包平均字节数进行匹配,计数器64位长。

该扩展可以用来检测维持了很长时间的下载,并将其安排到低优先级,以进行带宽控制。

使用 -m connbytes加载,可以接额外选项:

子选项 说明
[!] –connbytes from[:to] 指定匹配的封包/字节数范围
–connbytes-dir {original|reply|both} 统计哪种类型的封包
–connbytes-mode {packets|bytes|avgpkt} 统计模式,封包数、字节数、平均每封包的字节数

connlimit

可用于限制一个客户端可以连接到某个服务的并发连接数。可以接额外选项:

子选项 说明
–connlimit-upto n 如果连接数低于或者等于n则匹配
–connlimit-above n 如果连接数大于n则匹配
–connlimit-mask prefix_length 统计哪些IP的总和连接数,prefire_length指定分组的IP前缀
–connlimit-saddr 限制源地址组,默认值
–connlimit-daddr 限制目的地址组

示例:

1
2
3
4
# 允许每个客户端主机开启两个telnet连接
iptables -A INPUT -p tcp --syn --dport 23 -m connlimit --connlimit-above 2 -j REJECT
# 限制每个C类地址网段(例如192.168.0.*)最多总计建立16个HTTP连接
iptables -p tcp --syn --dport 80 -m connlimit --connlimit-above 16 --connlimit-mask 24 -j REJECT

connmark

基于关联到连接的netfilter mark字段进行匹配,此mark由CONNMARK目标来设置。

用法: -m connmark [!] --mark value[/mask]

示例:

1
2
3
#                            匹配所属连接被标记为123的封包
# 将连接的标记拷贝到封包上
iptables -t mangle -I OUTPUT -m connmark --mark 123 -j CONNMARK --restore-mark

connlabel

类似于connmark,但是标签是按位的(每个标签占一个bit),全系统支持最多128个标签

socket

匹配条件:

  1. 针对包进行套接字查找(Socket lookup),如果发现存在打开的TCP/UDP套接字,也就是说SRC_IP:SRC_PORT:DST_IP:DST_PORT作为已建立的(established)套接字存在于本地TCP/IP栈,则匹配
  2. 存在established的监听套接字,则匹配
  3. 存在非零绑定的监听套接字(non-zero bound listening socket),甚至在非本地地址上监听,则匹配。所谓zero bound应该就是指绑定到0.0.0.0

简而言之,就是匹配应当由本地TCP/IP栈处理(而非转发)的封包。进行套接字查找时,使用packet元组,或者嵌入在 ICMP/ICPMv6错误报文中的原始TCP/UDP头

可以接额外选项:

子选项 说明
–transparent 仅仅匹配透明套接字。透明套接字意味着地址不是本机的任何网络接口上配置的地址
–nowildcard 不忽略绑定到ANY地址(0.0.0.0)的套接字默认情况下不匹配zero-bound的监听套接字,其目的是让本地服务能够监听请求而不是统统被转发

这个匹配扩展+MARK+策略路由,就能够实现全功能的non-locally bound套接字。常常用于配合优化透明代理。通常是匹配后进行MARK,然后具有此MARK的封包路由设置到lo。

从各种文档中看到关于socket match的说明,都不是非常明朗,导致理解起TPROXY拦截模式下Istio的iptables规则有些困难:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# mangle表

target prot opt source destination
ISTIO_INBOUND tcp -- 0.0.0.0/0 0.0.0.0/0

Chain INPUT (policy ACCEPT)
target prot opt source destination

Chain FORWARD (policy ACCEPT)
target prot opt source destination

Chain OUTPUT (policy ACCEPT)
target prot opt source destination

Chain POSTROUTING (policy ACCEPT)
target prot opt source destination

Chain ISTIO_DIVERT (1 references)
target prot opt source destination
MARK all -- 0.0.0.0/0 0.0.0.0/0 MARK set 0x539
LOG tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 LOG flags 0 level 4 prefix "dst-80-socket-matched: "
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0

Chain ISTIO_INBOUND (1 references)
target prot opt source destination
RETURN tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
RETURN tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:15090
RETURN tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:15020
ISTIO_DIVERT tcp -- 0.0.0.0/0 0.0.0.0/0 socket
ISTIO_TPROXY tcp -- 0.0.0.0/0 0.0.0.0/0

Chain ISTIO_TPROXY (1 references)
target prot opt source destination
LOG tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 LOG flags 0 level 4 prefix "dst-80-before-tproxy: "
TPROXY tcp -- 0.0.0.0/0 !127.0.0.1 TPROXY redirect 0.0.0.0:15001 mark 0x539/0xffffffff

这是一个启用了TPROXY拦截模式的Pod的iptables,LOG目标是我添加用于分析socket match的行为的。此Pod的地址是172.27.121.156。可以看到它在监听:

1
2
3
4
# netstat -nlt
# Active Internet connections (only servers)
# Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN

现在我们从另一个客户端 172.27.155.65上访问 curl http://172.27.121.156

这会发起一个TCP连接,它的SYN包,是否匹配socket扩展呢?我们回顾一下它的匹配条件:

  1. 套接字查找成功,这个不满足。因为是新连接
  2. 已经建立的监听套接字,这个不满足,监听套接字还没有收到SYN,谈不上建立
  3. 存在non-zero bound listening socket。这个说的让人费解。但是肯定隐含着在SYN包的目的地址上监听。此SYN包的目的地址是172.27.121.156:80,我们知道Pod在0.0.0.0:80上监听了,那么172.27.121.156作为它的本地地址,自然也在其上监听。那么,这个监听套接字,是不是non-zero bound的呢?网上很难找到相关信息,我的理解,zero bound就是指绑定到0.0.0.0。在这种猜测之下,是不匹配socket的

根据iptables日志来验证一下。客户端curl之后,在Pod的内核看到一下日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 客户端发来的SYN,可以看到不匹配socket,直接走到TPROXY,这证明了上述猜测
dst-80-before-tproxy: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=60 SYN URGP=0
# 客户端发来的ACK,注意反方向报文没有日志
# 这此匹配socket了,因为满足了1,套接字查找成功
dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 ACK URGP=0 MARK=0x539
# 这次应该是客户端发送HTTP请求报文了
dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=130 ACK PSH URGP=0 MARK=0x539
# 这里是Envoy发给80端口进程,首个报文,但是不会TPROXY,因为目的地址不满足要求
dst-80-before-tproxy: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=60 TOS=0x00 PREC=0x00 SYN URGP=0
# 套接字查找成功,都不再走TPROXY了
dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=52 TOS=0x00 PREC=0x00 ACK URGP=0 MARK=0x539
dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=322 TOS=0x00 PREC=0x00 ACK PSH URGP=0 MARK=0x539
dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=52 TOS=0x00 PREC=0x00 ACK URGP=0 MARK=0x539
dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=52 TOS=0x00 PREC=0x00 ACK URGP=0 MARK=0x539
# 80进程处理完,报文返回Envoy,Envoy又返回给客户端。客户端发起后续报文,包括FIN
dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 TOS=0x00 ACK URGP=0 MARK=0x539
dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 TOS=0x00 ACK URGP=0 MARK=0x539
dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 TOS=0x00 ACK FIN URGP=0 MARK=0x539
dst-80-socket-matched: IN=eth0 OUT= SRC=172.27.155.65 DST=172.27.121.156 LEN=52 TOS=0x00 ACK URGP=0 MARK=0x539
dst-80-socket-matched: IN=lo OUT= SRC=127.0.0.1 DST=127.0.0.1 LEN=52 TOS=0x00 PREC=0x00 ACK FIN URGP=0 MARK=0x539

set

IPSet扩展,可以匹配一组IP。使用 -m set加载,额外选项:

子选项 说明
–match-set ipsetname 匹配的IP Set, [!] --match-set setname flag[,flag]...形式此选项必须,flag是IPSet的字段iptables -A FORWARD -m set --match-set test src,dst表示:如果test是ipportmap,则当封包的源地址、目的端口对可以在test中发现时,匹配。如果test是ipmap,则当封包的源地址可以在test中发现时,匹配。
–return-nomatch 如果指定此选项并且目标IPSet的类型支持nomatch,则反转匹配结果

bpf

使用Linux Socket Filter来过滤,需要提供十进制形式的(由nfbpf_compile生成)BPF字节码。

示例:

1
2
3
iptables -A OUTPUT -m bpf --bytecode '4,48 0 0 9,21 0 1 6,6 0 0 1,6 0 0 0' -j ACCEPT
# 等价写法
iptables -A OUTPUT -m bpf --bytecode "`nfbpf_compile RAW 'ip proto 6'`" -j ACCEPT

cpu

根据处理封包的CPU来匹配。示例:

1
2
3
4
# 如果是第一个核心处理了封包,则重定位到8080端口
iptables -t nat -A PREROUTING -p tcp --dport 80 -m cpu --cpu 0 -j REDIRECT --to-port 8080
# 如果是第二个核心处理了封包,则重定位到8081端口
iptables -t nat -A PREROUTING -p tcp --dport 80 -m cpu --cpu 1 -j REDIRECT --to-port 8081

time

根据封包到达时间来匹配。

目标

使用选项 -j来指定目标,除了本节要介绍的各种目标外,该选项的值还可以是:

  1. 自定义链的名称,这导致跳转到自定义的链继续执行

ACCEPT

接收封包,不再遍历当前链的其它规则、也不会在遍历当前表中的任何规则。但是,在一个链中被接收的封包,可能仍然需要经过其它表中的链,仍然可能被DROP。

在子链中ACCEPT的效果,等价于在父链中被ACCEPT。

RETURN

停止遍历当前链:

  1. 如果当前位于子链,则回到父链,继续执行后面的规则
  2. 如果当前位于主链,则执行默认策略

DROP

静默的丢弃封包,不会在对它进行任何后续处理。

可能会导致对方主机上出现一个僵尸套接字,最好用REJECT代替。

REJECT

用于发送一个错误报文给匹配封包的发送者,其它方面,该目标的行为与DROP一致:丢弃封包,终止规则遍历。

该目标仅用于INPUT, FORWARD, OUTPUT规则链中,或者从这两规则链调用的用户自定义规则链。

额外选项:

子选项 说明
–reject-with type 错误报文的类型: icmp-net-unreachable icmp-host-unreachabl icmp-port-unreachable(默认) icmp-proto-unreachable icmp-net-prohibited icmp-host-prohibite icmp-admin-prohibited

AUDIT

允许创建命中目标的封包的审计记录,用来记录被接受的、丢弃的、拒绝的封包。

用法: -j AUDIT –type {accept|drop|reject}

示例:

1
2
3
4
5
6
# 添加一个规则链到默认表格
iptables -N AUDIT_DROP
# 审计丢弃包的动作
iptables -A AUDIT_DROP -j AUDIT --type drop
# 丢弃包
iptables -A AUDIT_DROP -j DROP

LOG

这是一个非终止性目标,后续规则总是会被执行。如果你希望LOG后DROP,在此规则后面紧跟着一个相同匹配的目标为DROP的规则。

让内核记录匹配的封包。内核将会打印匹配的封包的信息,包括大部分IP头字段。这些日志可以在dmesg/syslog中看到。

额外选项:

选项 说明
–log-level level 日志级别:emerg, alert, crit, error, warning, notice, info, debug
–log-prefix prefix 日志前缀,29个字符最多
–log-tcp-sequence 记录TCP序列号
–log-tcp-options 记录TCP选项
–log-ip-options 记录IP选项
–log-uid 记录生成封包的UID

关于容器中的iptables logging:

  1. 默认情况下,来自容器(非全局网络命名空间)的iptable logging被忽略,你无法在 dmesg -w中看到
  2. 要改变此行为,你需要较新版本的内核,并且设置: echo 1 > /proc/sys/net/netfilter/nf_log_all_netns

DNAT

仅用于nat表的PREROUTING,OUTPUT规则链,或者从这两规则链调用的用户自定义规则链。

该目标指定封包的目的地址应当被修改(且该连接上所有以后的封包应当执行同样的操作),规则应当停止检查。

应用场景:HTTP服务放置在内网主机上,前端有个防火墙配置公网IP,互联网用户通过此公网IP访问内网主机上的HTTP服务,这种情况需要DNAT

额外选项:

子选项 说明
–to-destination –to-destination [ipaddr[-ipaddr]][:port[-port]]指定单个目的IP地址或者一个地址范围。如果指定了以下协议:tcp, udp, dccp, sctp,则可以指定一个端口范围。如果不指定端口,那么目的端口不会被改写。如果不指定IP地址,则仅仅会修改端口
–random 随机化端口映射
–persistent 对于一个客户端连接,给予相同的映射

举例:

1
2
3
4
5
6
7
# 如果公网客户端访问本防火墙的80端口
# 那么将其目标地址改写为192.168.0.200,目标端口不变
iptables -t nat
-A PREROUTING
-d 106.185.46.7
--dport 80
-j DNAT --to-destination 192.168.0.200xxxxxxxxxx # 如果公网客户端访问本防火墙的80端口# 那么将其目标地址改写为192.168.0.200,目标端口不变iptables -t nat         -A PREROUTING         -d 106.185.46.7         --dport 80         -j DNAT --to-destination 192.168.0.200Shell

SNAT

仅用于nat表的POSTROUTING,INPUT规则链,或者从这两规则链调用的用户自定义规则链。

该目标指定封包的源地址应当被修改(且该连接上所有以后的封包应当执行同样的操作),规则应当停止检查。

应用场景:局域网前端有个防火墙配置公网IP,内网主机通过该IP访问公网,为了内网主机能够访问外网,需要SNAT。

额外选项:

子选项 说明
–to-source –to-source [ipaddr[-ipaddr]][:port[-port]]指定单个源IP地址或者一个地址范围。如果指定了以下协议:tcp, udp, dccp, sctp,则可以指定一个端口范围。如果不指定端口范围,那么小于512的源端口映射到512以下;512-1023之间的源端口映射到1024以下;其它源端口映射到1024以上。如果可能,则不去修改端口
–random 随机化端口映射
–persistent 对于一个客户端连接,给予相同的映射

举例:

1
2
3
4
5
6
7
# 对于来自192.168.0.0网段的所有出站请求
# 如果这些封包将通过eth0接口(外网网卡)传出去
# 那么,将封包源地址改写为106.185.46.7(即eth0的IP地址)
iptables -t nat -A POSTROUTING
-s 192.168.0.0/255.255.255.0
-o eth0
-j SNAT --to-source 106.185.46.7

MASQUERADE

仅用于nat表的POSTROUTING规则链。应当用于动态分配IP的网络连接,静态网络连接应当使用SNAT目标。该目标会自动从网络接口上获取当前IP地址,用来做源地址的SNAT

应用场景:本主机具有动态分配的公网地址,有客户端通过PPP连接(VPN)到本主机,并通过本主机上网。这时需要将客户端封包的源地址(PPP对端地址)转换为公网地址,但是由于本主机的公网地址是动态获取的,因此不能使用SNAT,只能使用MASQUERADE

额外选项:

子选项 说明
–to-ports port[-port] 指定一组转换后使用的源端口。仅当协议指定为tcp, udp, dccp, sctp时可用
–random 随机化端口映射

举例:

1
2
3
4
iptables -t nat -A POSTROUTING 
-s 192.168.0.0/255.255.255.0
-o eth0
-j MASQUERADE

TPROXY

仅可用在mangle的PREROUTING链(或者从该链调用的用户自定义链)。此目标能够重定向包到一个本地套接字,但却不修改包头的任何信息。 它还能够改变封包的mark,进而辅助后续的策略路由。

额外选项:

子选项 说明
–on-port port 重定向到的本地套接字的目标端口,0表示新的目标端口与原始端口相同
–on-ip address 重定向到的本地套接字的目标IP地址,默认情况下是入口网卡的地址。仅仅当规则指定了-p tcp或-p udp时可用
–tproxy-mark value[/mask] 为封包添加标记,以便选择正确的路由表,这里设置的fwmark可以用于高级路由。透明代理必须要此选项正确设置,否则封包将被转发走

REDIRECT

可以认为是一种DNAT。

仅用于nat表的PREROUTING,OUTPUT规则链,或者从这两规则链调用的用户自定义规则链。该目标重定向封包到当前主机本身,通过把目的地址改写为入站网卡的主地址(本地生成的封包映射到localhost地址,对于IPv4即127.0.0.1)。

额外选项:

子选项 说明
–to-ports port[-port] 重定向到的目的端口,或者目的端口的范围如果不指定此选项,则目的端口不变
–random 随机化端口映射

CONNMARK

在连接上设置netfilter标记。标记为32bit。

额外选项:

子选项 说明
–set-xmark –set-xmark value[/mask]如果指定mask,则将mask提供的bit归零,并和value 异或,得到最终的ctmark
–save-mark –save-mark [–nfmask nfmask] [–ctmask ctmask]拷贝封包标记(nfmark)为连接标记(ctmark)。算法:ctmark = (ctmark & ~ctmask) ^ (nfmark & nfmask)
–restore-mark –restore-mark [–nfmask nfmask] [–ctmask ctmask]拷贝连接标记为封包标记。算法:nfmark = (nfmark & ~nfmask) ^ (ctmark & ctmask) 此选项仅能用于mangle表
–and-mark bits 等价于 –set-xmark 0/invbits,invbits是bits取反
–or-mark bits 等价于–set-xmark bits/bits
–xor-mark bits 等价于–set-xmark bits/0
–set-mark value[/mask] 仅仅mask中设置的那些bit,对应value中的bit的设置

MARK

在封包上设置netfilter标记。标记为32bit。

常常和基于fwmark的路由(需要iproute2)联用,这种情况下你需要在PREROUTING链mangle表中使用,以影响路由策略。

额外选项:

子选项 说明
–set-xmark value[/mask] 如果指定mask,则将当前nfmark中mask提供的bit归零,并和value 异或,得到最终的nfmark
–set-mark value[/mask] 如果指定mask,则将当前nfmark中mask提供的bit归零,并和value或,得到最终的nfmark
–and-mark bits 和nfmark进行二进制与
–or-mark bits 和nfmark进行二进制或
–xor-mark bits 和nfmark进行二进制异或

CHECKSUM

只能用于mangle表,可以为封包添加缺失的校验和

IDLETIMER

可以用于识别某个网络接口已经空闲一定的时间

NETMAP

允许静态的把整个网络的IP地址映射到另外一个网络的IP地址

NFLOG

用于记录匹配包的日志

NOTRACK

禁止匹配规则的封包的连接跟踪。你无法通过conntrack命令看到这样的连接

TCPMSS

修改TCP的SYN封包的MSS值,用来控制连接的最大单个段的大小,一般限制到出站网络接口MTU-40

TTL

该目标用于修改封包的TTL

高级

conntrack和NAT

假设内网客户端机器IP地址为10.0.0.1;NAT网关内网地址10.0.0.2,外网地址10.4.80.14;外网服务器地址为9.135.102.75。

我们可以通过conntrack命令,在NAT网关上查看相关事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
conntrack -E --proto tcp -d 9.135.102.75

[NEW] tcp 6 120 SYN_SENT src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000
[UNREPLIED] src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736
[UPDATE] tcp 6 60 SYN_RECV src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000
src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736
[UPDATE] tcp 6 432000 ESTABLISHED src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000
src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED]
[UPDATE] tcp 6 120 FIN_WAIT src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000
src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED]
[UPDATE] tcp 6 60 CLOSE_WAIT src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000
src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED]
[UPDATE] tcp 6 30 LAST_ACK src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000
src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED]
[UPDATE] tcp 6 120 TIME_WAIT src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000
src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED]
# 过了2MSL
[DESTROY] tcp 6 src=10.0.0.1 dst=9.135.102.75 sport=56736 dport=36000
src=9.135.102.75 dst=10.4.80.14 sport=36000 dport=56736 [ASSURED]

NAT表的特殊之处在于,仅新连接的第一个封包会经过此表。第一个封包的地址转换结果,会应用到连接的所有后续封包。

NAT网关接收到新连接后,创建conntrack条目:

10.0.0.1:56736->9.135.102.75:36000, 9.135.102.75:36000-> 10.0.0.1:56736

在iptables中,NAT网关的POSTROUTING钩子中的SNAT规则,会修改conntrack的应答目的地址

10.0.0.1:56736->9.135.102.75:36000, 9.135.102.75:36000->10.4.80.14:56736

这时,就到达上面命令输出的[NEW]状态了。请求/应答报文进行地址转换所需的信息已经完备。

iptables规则所做的事情,仅仅是修改conntrack规则而已。所有其它事情,包括请求封包的源地址修改,是由内核中的conntrack模块(nf_conntrack、nf_conntrack_ipv4)以及nat模块(nf_nat、nf_nat_ipv4)等负责的。

当NAT网关接收到应答报文后,会检索conntrack中的条目,并将匹配的条目关联到该封包( skb->_nfct)。这个条目将用于目的地址的还原。

注意以下几点:

  1. iptables不是netfilter本身,它是netfilter的用户。而conntrack和nat模块是netfilter的一部分
  2. 无法在POSTROUTING钩子+NAT表看到检查到应答报文,因为NAT表仅仅对新连接调用一次
  3. 连接跟踪、NAT转换的大部分工作由相应内核模块,而不是iptables完成

关于xtables锁

实现方式

在iptables新老版本中,xtables锁的实现不同。在1.4.x中,锁基于abstract unix domain socket实现;1.6.x中则基于普通文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
func grabIptablesLocks(lockfilePath string) (iptablesLocker, error) {
var err error
var success bool

l := &locker{}
defer func(l *locker) {
// Clean up immediately on failure
if !success {
l.Close()
}
}(l)

// iptables 1.6.x和1.4.x版本的锁是不一样的
// 1.6.x的锁文件是:/run/xtables.lock
// 下面的逻辑是1.6.x xtables_lock() 函数的粗略的重复实现
l.lock16, err = os.OpenFile(lockfilePath, os.O_CREATE, 0600)
if err != nil {
return nil, fmt.Errorf("failed to open iptables lock %s: %v", lockfilePath, err)
}


// 尝试加锁(1.6.x版本)
if err := wait.PollImmediate(200*time.Millisecond, 2*time.Second, func() (bool, error) {
if err := grabIptablesFileLock(l.lock16); err != nil {
return false, nil
}
return true, nil
}); err != nil {
return nil, fmt.Errorf("failed to acquire new iptables lock: %v", err)
}

// 下面的逻辑是1.4.x xtables_lock() 函数的粗略的重复实现
if err := wait.PollImmediate(200*time.Millisecond, 2*time.Second, func() (bool, error) {
// 1.4.x的锁是抽象Unidx Domain socket
// 前缀的@表示套接字位于抽象命名空间(abstract namespace),不体现为文件系统中的文件
l.lock14, err = net.ListenUnix("unix", &net.UnixAddr{Name: "@xtables", Net: "unix"})
if err != nil {
return false, nil
}
return true, nil
}); err != nil {
return nil, fmt.Errorf("failed to acquire old iptables lock: %v", err)
}

success = true
return l, nil
}

// 加1.6锁
func grabIptablesFileLock(f *os.File) error {
// 独占 非阻塞
return unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB)
}

加锁失败时的错误

尝试修改iptables时,可能会因为锁被占用导致报错。这种报错没有专门的退出码,因此只能比对stderr输出来判定。

正常情况下,出现锁争用的概率比较低,可以使用下面的命令显式占用xtables锁:

1
2
3
4
5
# 1.4.x加锁
socat ABSTRACT-LISTEN:xtables -

# 1.6.x加锁
flock -x /run/xtables.lock sleep 365d

保持运行上述两个命令的Terminal打开,然后插入iptables规则。可以看到报错信息如下:

  1. v1.4.21版本:
    1. 不带-w参数:Another app is currently holding the xtables lock. Perhaps you want to use the -w option?
    2. 带有-w参数:Another app is currently holding the xtables lock; still 21s 0us time ahead to have a chance to grab the lock… Another app is currently holding the xtables lock. Stopped waiting after 30s.
  2. v1.6.0版本:报错同上

实际上,K8S中就是基于上述输出信息进行判断的:

1
2
3
func IsProxyLocked(err error) bool {
return strings.Contains(err.Error(), "holding the xtables lock")
}